﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using static LightCAD.MathLib.Constants;
using static LightCAD.Three.TypeUtils;
using LightCAD.Three.OpenGL;
using System.Text.RegularExpressions;
using LightCAD.MathLib;

namespace LightCAD.Three
{
    public class UniformContainer
    {
        public ListEx<IUniform> seq = new ListEx<IUniform>();
        public JsObj<IUniform> map = new JsObj<IUniform>();
    }

    public static class UniformSetters
    {
        public static JsObj<int, double[]> arrayCacheF32 = new JsObj<int, double[]>();
        public static JsObj<int, int[]> arrayCacheI32 = new JsObj<int, int[]>();

        public static double[] mat4array = new double[16];
        public static double[] mat3array = new double[9];
        public static double[] mat2array = new double[4];

        public static Texture emptyTexture = /*@__PURE__*/ new Texture();
        public static DataArrayTexture emptyArrayTexture = /*@__PURE__*/ new DataArrayTexture();
        public static Data3DTexture empty3dTexture = /*@__PURE__*/ new Data3DTexture();
        public static CubeTexture emptyCubeTexture = /*@__PURE__*/ new CubeTexture();

        private static double[] flatten(Array array, int nBlocks, int blockSize)
        {
            if (array is double[])
            {
                return (double[])array;
            }
            var firstElem = array.GetValue(0);


            var n = nBlocks * blockSize;
            var r = arrayCacheF32[n];

            if (!arrayCacheF32.has(n))
            {

                r = new double[n];
                arrayCacheF32[n] = r;

            }

            var farr = r;
            if (nBlocks != 0)
            {
                if (firstElem.HasMethod("toArray"))
                {
                    firstElem.InvokeMethod("toArray", farr, 0);

                    for (int i = 1, offset = 0; i != nBlocks; ++i)
                    {

                        offset += blockSize;
                        array.GetValue(i).InvokeMethod("toArray", farr, offset);

                    }
                }
            }

            return farr;
        }

        private static int[] allocTexUnits(WebGLTextures textures, int n)
        {

            var r = arrayCacheI32[n];

            if (!arrayCacheI32.has(n))
            {

                r = new int[n];
                arrayCacheI32[n] = r;

            }
            var iarr = (int[])r;
            for (int i = 0; i != n; ++i)
                iarr[i] = textures.allocateTextureUnit();

            return iarr;

        }

        /// <summary>
        /// Helper to pick the right setter for the singular case
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Action<UniformItem, object, WebGLTextures> getSingularSetter(int type)
        {
            switch (type)
            {
                case 0x1406: return setValueV1f; // FLOAT
                case 0x8b50: return setValueV2f; // _VEC2
                case 0x8b51: return setValueV3f; // _VEC3
                case 0x8b52: return setValueV4f; // _VEC4

                case 0x8b5a: return setValueM2; // _MAT2
                case 0x8b5b: return setValueM3; // _MAT3
                case 0x8b5c: return setValueM4; // _MAT4

                case 0x1404: case 0x8b56: return setValueV1i; // INT, BOOL
                case 0x8b53: case 0x8b57: return setValueV2i; // _VEC2
                case 0x8b54: case 0x8b58: return setValueV3i; // _VEC3
                case 0x8b55: case 0x8b59: return setValueV4i; // _VEC4

                case 0x1405: return setValueV1ui; // UINT
                case 0x8dc6: return setValueV2ui; // _VEC2
                case 0x8dc7: return setValueV3ui; // _VEC3
                case 0x8dc8: return setValueV4ui; // _VEC4

                case 0x8b5e: // SAMPLER_2D
                case 0x8d66: // SAMPLER_EXTERNAL_OES
                case 0x8dca: // INT_SAMPLER_2D
                case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
                case 0x8b62: // SAMPLER_2D_SHADOW
                    return setValueT1;

                case 0x8b5f: // SAMPLER_3D
                case 0x8dcb: // INT_SAMPLER_3D
                case 0x8dd3: // UNSIGNED_INT_SAMPLER_3D
                    return setValueT3D1;

                case 0x8b60: // SAMPLER_CUBE
                case 0x8dcc: // INT_SAMPLER_CUBE
                case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
                case 0x8dc5: // SAMPLER_CUBE_SHADOW
                    return setValueT6;

                case 0x8dc1: // SAMPLER_2D_ARRAY
                case 0x8dcf: // INT_SAMPLER_2D_ARRAY
                case 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY
                case 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW
                    return setValueT2DArray1;

            }
            return null;
        }


        /// <summary>
        /// Helper to pick the right setter for a pure (bottom-level) array
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Action<UniformItem, object, WebGLTextures> getPureArraySetter(int type)
        {
            switch (type)
            {

                case 0x1406: return setValueV1fArray; // FLOAT
                case 0x8b50: return setValueV2fArray; // _VEC2
                case 0x8b51: return setValueV3fArray; // _VEC3
                case 0x8b52: return setValueV4fArray; // _VEC4

                case 0x8b5a: return setValueM2Array; // _MAT2
                case 0x8b5b: return setValueM3Array; // _MAT3
                case 0x8b5c: return setValueM4Array; // _MAT4

                case 0x1404: case 0x8b56: return setValueV1iArray; // INT, BOOL
                case 0x8b53: case 0x8b57: return setValueV2iArray; // _VEC2
                case 0x8b54: case 0x8b58: return setValueV3iArray; // _VEC3
                case 0x8b55: case 0x8b59: return setValueV4iArray; // _VEC4

                case 0x1405: return setValueV1uiArray; // UINT
                case 0x8dc6: return setValueV2uiArray; // _VEC2
                case 0x8dc7: return setValueV3uiArray; // _VEC3
                case 0x8dc8: return setValueV4uiArray; // _VEC4

                case 0x8b5e: // SAMPLER_2D
                case 0x8d66: // SAMPLER_EXTERNAL_OES
                case 0x8dca: // INT_SAMPLER_2D
                case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
                case 0x8b62: // SAMPLER_2D_SHADOW
                    return setValueT1Array;

                case 0x8b5f: // SAMPLER_3D
                case 0x8dcb: // INT_SAMPLER_3D
                case 0x8dd3: // UNSIGNED_INT_SAMPLER_3D
                    return setValueT3DArray;

                case 0x8b60: // SAMPLER_CUBE
                case 0x8dcc: // INT_SAMPLER_CUBE
                case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
                case 0x8dc5: // SAMPLER_CUBE_SHADOW
                    return setValueT6Array;

                case 0x8dc1: // SAMPLER_2D_ARRAY
                case 0x8dcf: // INT_SAMPLER_2D_ARRAY
                case 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY
                case 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW
                    return setValueT2DArrayArray;

            }
            return null;

        }

        // --- Setters ---

        // Note: Defining these methods externally, because they come in a bunch
        // and this way their names minify.

        #region Singular

        // Single scalar
        public static Action<UniformItem, object, WebGLTextures> setValueV1f = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = new double[] { Convert.ToDouble(v) };
            if (cache.floatToFloatEqual(data)) return;
            gl.uniform1f(_this.addr, Convert.ToSingle(v));
            _this.cache = data;

        };

        // Single float vector (from flat array or THREE.VectorN)
        public static Action<UniformItem, object, WebGLTextures> setValueV2f = (_this, v, textures) =>
        {

            var cache = _this.cache;

            if (v is Vector2)
            {
                var v2 = v as Vector2;
                var varr = v2.ToArray();
                if (!varr.floatToFloatEqual(cache))
                {

                    gl.uniform2f(_this.addr, (float)v2.X, (float)v2.Y);

                    _this.cache = v2.ToArray();
                }
            }
            else
            {
                var varr = (double[])v;
                if (varr.floatToFloatEqual(cache)) return;

                gl.uniform2fv(_this.addr, varr.doubleToFloat());

                _this.cache = copyFloatArray(varr);
            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3f = (_this, v, textures) =>
        {

            var cache = _this.cache;

            if (v is Vector3)
            {
                var v3 = v as Vector3;
                var varr = v3.ToArray();
                if (!varr.floatToFloatEqual(cache))
                {

                    gl.uniform3f(_this.addr, (float)v3.X, (float)v3.Y, (float)v3.Z);

                    _this.cache = varr;

                }

            }
            else if (v is Color)
            {
                var vc = v as Color;
                var vcArr = vc.ToArray(null, 0);
                if (!vcArr.floatToFloatEqual(cache))
                {

                    gl.uniform3f(_this.addr, (float)vcArr[0], (float)vcArr[1], (float)vcArr[2]);

                    _this.cache = vcArr;

                }

            }
            else
            {
                double[] varr;
                if (v is ListEx<double>)
                    varr = (v as ListEx<double>).ToArray();
                else
                    varr = (double[])v;

                if (varr.floatToFloatEqual(cache)) return;

                gl.uniform3fv(_this.addr, varr.doubleToFloat());

                _this.cache = copyFloatArray(varr);

            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueV4f = (_this, v, textures) =>
        {

            var cache = _this.cache;

            if (v is Vector4)
            {
                var v4 = v as Vector4;
                var varr = v4.ToArray();
                if (!varr.floatToFloatEqual(cache))
                {

                    gl.uniform4f(_this.addr, (float)v4.X, (float)v4.Y, (float)v4.Z, (float)v4.W);

                    _this.cache = v4.ToArray();

                }

            }
            else
            {
                var varr = (double[])v;

                if (varr.floatToFloatEqual(cache)) return;

                gl.uniform4fv(_this.addr, varr.doubleToFloat());

                _this.cache = copyFloatArray(varr);
            }

        };


        // Single matrix (from flat array or THREE.MatrixN)
        public static Action<UniformItem, object, WebGLTextures> setValueM2 = (_this, v, textures) =>
        {

            var cache = _this.cache;
            if (v is Matrix2)
            {
                var elements = (v as Matrix2).elements;
                if (elements.floatToFloatEqual(cache)) return;

                gl.uniformMatrix2fv(_this.addr, false, elements.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }
            else
            {
                var elements = (double[])v;

                if (elements.floatToFloatEqual(cache)) return;

                elements.CopyTo(mat2array, 0);//ensure 4

                gl.uniformMatrix2fv(_this.addr, false, mat2array.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }

        };

        public static Action<UniformItem, object, WebGLTextures> setValueM3 = (_this, v, textures) =>
        {
            var cache = _this.cache;

            if (v is Matrix3)
            {
                var elements = (v as Matrix3).Elements;
                if (elements.floatToFloatEqual(cache)) return;

                gl.uniformMatrix3fv(_this.addr, false, elements.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }
            else
            {

                var elements = (double[])v;
                if (elements.floatToFloatEqual(cache)) return;

                elements.CopyTo(mat3array, 0);//ensure 9

                gl.uniformMatrix3fv(_this.addr, false, mat3array.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }

        };

        public static Action<UniformItem, object, WebGLTextures> setValueM4 = (_this, v, textures) =>
        {

            var cache = _this.cache;

            if (v is Matrix4)
            {

                var elements = (v as Matrix4).Elements;
                if (elements.floatToFloatEqual(cache)) return;

                gl.uniformMatrix4fv(_this.addr, false, elements.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }
            else
            {
                var elements = (double[])v;
                if (elements.floatToFloatEqual(cache)) return;

                elements.CopyTo(mat4array, 0);//ensure 16

                gl.uniformMatrix4fv(_this.addr, false, mat4array.doubleToFloat());

                _this.cache = copyFloatArray(elements);

            }

        };


        // Single integer / boolean

        public static Action<UniformItem, object, WebGLTextures> setValueV1i = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var iv = Convert.ToInt32(v);
            if (cache.Length == 1 && cache[0] == iv) return;

            gl.uniform1i(_this.addr, iv);

            cache = new double[] { iv };

        };


        // Single integer / boolean vector (from flat array or THREE.VectorN)

        public static Action<UniformItem, object, WebGLTextures> setValueV2i = (_this, v, textures) =>
        {

            var cache = _this.cache;
            if (v is Vector2)
            {
                var vec = (Vector2)v;
                if (cache.Length != 2 || cache[0] != vec.X || cache[1] != vec.Y)
                {
                    if (cache.Length != 2)
                    {
                        _this.cache = new double[2];
                        cache = _this.cache;
                    }
                    gl.uniform2i(_this.addr, (int)vec.X, (int)vec.Y);

                    cache[0] = vec.X;
                    cache[1] = vec.Y;
                }
            }
            else
            {
                var iarr = (int[])v;
                if (iarr[0] == cache[0] && iarr[1] == cache[1]) return;

                gl.uniform2iv(_this.addr, iarr);

                cache = new double[] { iarr[0], iarr[1] };
            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3i = (_this, v, textures) =>
        {
            var cache = _this.cache;
            if (v is Vector3)
            {
                var vec = (Vector3)v;
                if (cache[0] != vec.X || cache[1] != vec.Y || cache[2] != vec.Z)
                {

                    gl.uniform3i(_this.addr, (int)vec.X, (int)vec.Y, (int)vec.Z);

                    cache[0] = vec.X;
                    cache[1] = vec.Y;
                    cache[2] = vec.Z;
                }
            }
            else
            {
                var iarr = (int[])v;
                if (iarr[0] == cache[0] && iarr[1] == cache[1] && iarr[2] == cache[2]) return;

                gl.uniform3iv(_this.addr, iarr);

                cache = new double[] { iarr[0], iarr[1], iarr[2] };
            }

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV4i = (_this, v, textures) =>
        {
            var cache = _this.cache;
            if (v is Vector4)
            {
                var vec = (Vector4)v;
                if (cache[0] != vec.X || cache[1] != vec.Y || cache[2] != vec.Z || cache[3] != vec.W)
                {

                    gl.uniform4i(_this.addr, (int)vec.X, (int)vec.Y, (int)vec.Z, (int)vec.W);

                    cache[0] = vec.X;
                    cache[1] = vec.Y;
                    cache[2] = vec.Z;
                    cache[2] = vec.W;
                }
            }
            else
            {
                var iarr = (int[])v;
                if (iarr[0] == cache[0] && iarr[1] == cache[1] && iarr[2] == cache[2] && iarr[3] == cache[3]) return;

                gl.uniform4iv(_this.addr, iarr);

                cache = new double[] { iarr[0], iarr[1], iarr[2], iarr[3] };
            }

        };


        // Single unsigned integer

        public static Action<UniformItem, object, WebGLTextures> setValueV1ui = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var iv = Convert.ToUInt32(v);
            if (cache.Length == 1 && cache[0] == iv) return;

            gl.uniform1ui(_this.addr, iv);

            cache = new double[] { iv };

        };


        // Single unsigned integer vector (from flat array or THREE.VectorN)

        public static Action<UniformItem, object, WebGLTextures> setValueV2ui = (_this, v, textures) =>
        {

            var cache = _this.cache;
            if (v is Vector2)
            {
                var vec = (Vector2)v;
                if (cache[0] != vec.X || cache[1] != vec.Y)
                {

                    gl.uniform2ui(_this.addr, (uint)vec.X, (uint)vec.Y);

                    cache[0] = vec.X;
                    cache[1] = vec.Y;
                }
            }
            else
            {
                var iarr = (uint[])v;
                if (iarr[0] == cache[0] && iarr[1] == cache[1]) return;

                gl.uniform2uiv(_this.addr, iarr);

                cache = new double[] { iarr[0], iarr[1] };
            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3ui = (_this, v, textures) =>
       {
           var cache = _this.cache;
           if (v is Vector3)
           {
               var vec = (Vector3)v;
               if (cache[0] != vec.X || cache[1] != vec.Y || cache[2] != vec.Z)
               {

                   gl.uniform3ui(_this.addr, (uint)vec.X, (uint)vec.Y, (uint)vec.Z);

                   cache[0] = vec.X;
                   cache[1] = vec.Y;
                   cache[2] = vec.Z;
               }
           }
           else
           {
               var iarr = (uint[])v;
               if (iarr[0] == cache[0] && iarr[1] == cache[1] && iarr[2] == cache[2]) return;

               gl.uniform3uiv(_this.addr, iarr);

               cache = new double[] { iarr[0], iarr[1], iarr[2] };
           }

       };

        public static Action<UniformItem, object, WebGLTextures> setValueV4ui = (_this, v, textures) =>
        {
            var cache = _this.cache;
            if (v is Vector4)
            {
                var vec = (Vector4)v;
                if (cache[0] != vec.X || cache[1] != vec.Y || cache[2] != vec.Z || cache[3] != vec.W)
                {

                    gl.uniform4i(_this.addr, (uint)vec.X, (uint)vec.Y, (uint)vec.Z, (uint)vec.W);

                    cache[0] = vec.X;
                    cache[1] = vec.Y;
                    cache[2] = vec.Z;
                    cache[2] = vec.W;
                }
            }
            else
            {
                var iarr = (uint[])v;
                if (iarr[0] == cache[0] && iarr[1] == cache[1] && iarr[2] == cache[2] && iarr[3] == cache[3]) return;

                gl.uniform4uiv(_this.addr, iarr);

                cache = new double[] { iarr[0], iarr[1], iarr[2], iarr[3] };
            }

        };

        // Single texture (2D / Cube)
        public static Action<UniformItem, object, WebGLTextures> setValueT1 = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var unit = textures.allocateTextureUnit();

            if (cache.Length == 0 || cache[0] != unit)
            {
                gl.uniform1i(_this.addr, unit);
                _this.cache = new double[] { unit };

            }
            if (v == null)
            {
                v = emptyTexture;
            }

            textures.setTexture2D((Texture)v, unit);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueT3D1 = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var unit = textures.allocateTextureUnit();

            if (cache.Length == 0 || cache[0] != unit)
            {
                gl.uniform1i(_this.addr, unit);
                _this.cache = new double[] { unit };

            }

            if (v == null)
            {
                v = empty3dTexture;
            }
            textures.setTexture3D((Texture)v, unit);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueT6 = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var unit = textures.allocateTextureUnit();

            if (cache.Length == 0 || cache[0] != unit)
            {
                gl.uniform1i(_this.addr, unit);
                _this.cache = new double[] { unit };

            }
            if (v == null)
            {
                v = emptyCubeTexture;
            }
            textures.setTextureCube((Texture)v, unit);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueT2DArray1 = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var unit = textures.allocateTextureUnit();

            if (cache.Length == 0 || cache[0] != unit)
            {
                gl.uniform1i(_this.addr, unit);
                _this.cache = new double[] { unit };

            }
            if (v == null)
            {
                v = emptyArrayTexture;
            }
            textures.setTexture2DArray((Texture)v, unit);

        };

        #endregion Singular

        #region PureArray

        // Array of scalars
        public static Action<UniformItem, object, WebGLTextures> setValueV1fArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            double[] data = null;
            if (v is IList)
            {
                if (v is IList<double>)
                {
                    if (v is double[])
                        data = v as double[];
                    else
                    {
                        var vl = v as IList<double>;
                        data = vl.ToArray();
                    }
                }
                else
                {
                    var vl = v as IList;
                    data = new double[vl.Count];
                    for (int i = 0; i < vl.Count; i++)
                    {
                        data[i] = (double)vl[i];
                    }
                }
                if (cache.floatToFloatEqual(data))
                    return;
            }
            else
            {
                var iv = Convert.ToDouble(v);
                if (cache.Length == 1 && cache[0] == iv)
                    return;
                data = new double[] { iv };
            }
            gl.uniform1fv(_this.addr, data.doubleToFloat());

            _this.updateCache(data);

        };

        // Array of vectors (from flat array or array of THREE.VectorN)
        public static Action<UniformItem, object, WebGLTextures> setValueV2fArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = flatten((Array)v, _this.size, 2);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniform2fv(_this.addr, data.doubleToFloat());

            _this.updateCache(data);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3fArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            Array arr = null;
            if (v is Array)
            {
                arr = v as Array;
            }
            else
            {
                if (v.HasMethod("ToArray"))
                {
                    arr = v.InvokeMethod("ToArray") as Array;
                }
            }

            var data = flatten((Array)arr, _this.size, 3);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniform3fv(_this.addr, data.doubleToFloat());

            _this.updateCache(data);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV4fArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = flatten((Array)v, _this.size, 4);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniform4fv(_this.addr, data.doubleToFloat());

            _this.updateCache(data);

        };

        // Array of matrices (from flat array or array of THREE.MatrixN)
        public static Action<UniformItem, object, WebGLTextures> setValueM2Array = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = flatten((Array)v, _this.size, 4);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniformMatrix2fv(_this.addr, false, data.doubleToFloat());

            _this.updateCache(data);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueM3Array = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = flatten((Array)v, _this.size, 9);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniformMatrix3fv(_this.addr, false, data.doubleToFloat());

            _this.updateCache(data);

        };

        public static Action<UniformItem, object, WebGLTextures> setValueM4Array = (_this, v, textures) =>
        {

            var cache = _this.cache;
            double[] data;
            if (v is ListEx<Matrix4>)
                data = flatten(((ListEx<Matrix4>)v).ToArray(), _this.size, 16);
            else
                data = flatten((Array)v, _this.size, 16);

            if (data.floatToFloatEqual(cache)) return;

            gl.uniformMatrix4fv(_this.addr, false, data.doubleToFloat());

            _this.updateCache(data);

        };

        // Array of integer / boolean
        public static Action<UniformItem, object, WebGLTextures> setValueV1iArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var iv = (int)v;

            if (cache.Length == 1 && cache[0] == iv) return;

            gl.uniform1iv(_this.addr, new int[] { iv });

            _this.updateCache(new double[] { iv });

        };

        // Array of integer / boolean vectors (from flat array)
        public static Action<UniformItem, object, WebGLTextures> setValueV2iArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (int[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform2iv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3iArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (int[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform3iv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV4iArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (int[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform4iv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        // Array of unsigned integer
        public static Action<UniformItem, object, WebGLTextures> setValueV1uiArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var iv = (uint)v;

            if (cache.Length == 1 && cache[0] == iv) return;

            gl.uniform1uiv(_this.addr, new uint[] { iv });

            _this.updateCache(new double[] { iv });

        };

        // Array of unsigned integer vectors (from flat array)
        public static Action<UniformItem, object, WebGLTextures> setValueV2uiArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (uint[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform2uiv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV3uiArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (uint[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform3uiv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        public static Action<UniformItem, object, WebGLTextures> setValueV4uiArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            var data = (uint[])v;

            if (cache.floatToIntEqual(data)) return;

            gl.uniform4uiv(_this.addr, data);

            _this.updateCache(data.intToDouble());

        };

        // Array of textures (2D / 3D / Cube / 2DArray)
        public static Action<UniformItem, object, WebGLTextures> setValueT1Array = (_this, v, textures) =>
        {

            var cache = _this.cache;
            Texture[] varr;
            if (v is ListEx<Texture>)
                varr = (v as ListEx<Texture>).ToArray();
            else
                varr = (Texture[])v;

            var n = varr.Length;

            var units = allocTexUnits(textures, n);

            if (!_this.cache.floatToIntEqual(units))
            {

                gl.uniform1iv(_this.addr, units);
                _this.cache = new double[units.Length];
                _this.cache.floatCopyFormInt(units);

            }

            for (var i = 0; i != n; ++i)
            {
                var vi = varr[i];
                if (vi == null)
                    vi = emptyTexture;
                textures.setTexture2D(vi, units[i]);

            }

        };

        public static Action<UniformItem, object, WebGLTextures> setValueT3DArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            Data3DTexture[] varr;
            if (v is ListEx<Data3DTexture>)
                varr = (v as ListEx<Data3DTexture>).ToArray();
            else
                varr = (Data3DTexture[])v;
            var n = varr.Length;

            var units = allocTexUnits(textures, n);

            if (cache.floatToIntEqual(units) == false)
            {

                gl.uniform1iv(_this.addr, units);
                cache.floatCopyFormInt(units);

            }

            for (var i = 0; i != n; ++i)
            {
                var vi = varr[i];
                if (vi == null)
                    vi = empty3dTexture;
                textures.setTextureCube(vi, units[i]);

            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueT6Array = (_this, v, textures) =>
        {

            var cache = _this.cache;
            CubeTexture[] varr;
            if (v is ListEx<CubeTexture>)
                varr = (v as ListEx<CubeTexture>).ToArray();
            else
                varr = (CubeTexture[])v;

            var n = varr.Length;

            var units = allocTexUnits(textures, n);

            if (cache.floatToIntEqual(units) == false)
            {

                gl.uniform1iv(_this.addr, units);
                cache.floatCopyFormInt(units);
            }

            for (var i = 0; i != n; ++i)
            {
                var vi = varr[i];
                if (vi == null)
                    vi = emptyCubeTexture;
                textures.setTextureCube(vi, units[i]);
            }
        };

        public static Action<UniformItem, object, WebGLTextures> setValueT2DArrayArray = (_this, v, textures) =>
        {

            var cache = _this.cache;
            DataArrayTexture[] varr;
            if (v is ListEx<DataArrayTexture>)
                varr = (v as ListEx<DataArrayTexture>).ToArray();
            else
                varr = (DataArrayTexture[])v;

            var n = varr.Length;

            var units = allocTexUnits(textures, n);

            if (cache.floatToIntEqual(units) == false)
            {

                gl.uniform1iv(_this.addr, units);
                cache.floatCopyFormInt(units);
            }

            for (var i = 0; i != n; ++i)
            {
                var vi = varr[i];
                if (vi == null)
                    vi = emptyArrayTexture;
                textures.setTextureCube(vi, units[i]);
            }
        };

        #endregion PureArray
    }
    //public interface IUniform
    //{
    //    string id { get; }
    //    void setValue(object v, WebGLTextures textures);
    //}

    public abstract class UniformItem : IUniform
    {
        protected string _id;
        public int addr;
        public double[] cache;
        public int size;
        public Action<UniformItem, object, WebGLTextures> setValueFunc;

        public abstract void updateCache(double[] data);

        public void setValue(object v, WebGLTextures textures)
        {
            setValueFunc(this, v, textures);
        }
        public string id
        {
            get { return this._id; }
        }
    }

    public class SingleUniform : UniformItem
    {

        public SingleUniform(string id, WebGLActiveInfo activeInfo, int addr)
        {
            this._id = id;
            this.addr = addr;
            this.cache = new double[0];
            this.setValueFunc = UniformSetters.getSingularSetter(activeInfo.type);

            // this.path = activeInfo.name; // DEBUG

        }

        public override void updateCache(double[] data)
        {

        }
    }

    public class PureArrayUniform : UniformItem
    {
        public PureArrayUniform(string id, WebGLActiveInfo activeInfo, int addr)
        {
            this._id = id;
            this.addr = addr;
            this.size = activeInfo.size;
            this.setValueFunc = UniformSetters.getPureArraySetter(activeInfo.type);
            this.cache = new double[0];
            // this.path = activeInfo.name; // DEBUG
        }

        public override void updateCache(double[] data)
        {
            if (this.cache.Length != data.Length)
            {
                this.cache = new double[data.Length];
            }

            data.CopyTo(this.cache, 0);
        }

    }


    public class StructuredUniform : UniformContainer, IUniform
    {
        public string _id;
        public StructuredUniform(string id)
        {
            this._id = id;

        }
        public string id
        {
            get { return _id; }
        }
        public void setValue(object value, WebGLTextures textures)
        {
            if (value is IList)
            {
                var ovalue = (IList)value;

                for (int i = 0, n = this.seq.Count; i != n; ++i)
                {
                    var u = this.seq[i];
                    u.setValue(ovalue[int.Parse(u.id)], textures);
                }
            }
            else if (value is JsObj<object>)
            {
                var ovalue = (JsObj<object>)value;
                for (int i = 0, n = this.seq.Count; i != n; ++i)
                {
                    var u = this.seq[i];
                    u.setValue(ovalue[u.id], textures);
                }
            }
            else
            {
                for (int i = 0, n = this.seq.Count; i != n; ++i)
                {
                    var u = this.seq[i];
                    u.setValue(value.GetField(u.id), textures);
                }
            }
        }
    }

    public class WebGLUniforms : UniformContainer
    {
        public WebGLUniforms(int program)
        {

            this.seq = new ListEx<IUniform>();
            this.map = new JsObj<IUniform>();

            int n = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);

            for (int i = 0; i < n; ++i)
            {

                var info = gl.getActiveUniform(program, i);
                var addr = gl.getUniformLocation(program, info.name);
                parseUniform(info, addr, this);

            }

        }

        public static void parseUniform(WebGLActiveInfo activeInfo, int addr, UniformContainer container)
        {

            string path = activeInfo.name;
            int pathLength = path.Length;

            // reset RegExp object, because of the early exit of a previous run
            var RePathPart = new Regex(@"([\w\d_]+)(\])?(\[|\.)?");

            var matches = RePathPart.Matches(path);
            var last = matches[matches.Count - 1];
            var matchEnd = last.Index + last.Value.Length;

            if (matches.Count == 1)
            {
                var id = matches[0].Value;
                UniformItem uniform = new SingleUniform(id, activeInfo, addr);
                addUniform(container, uniform);
            }
            else if (matches.Count == 2)
            {
                var id = matches[0].Value.Trim('[', ']');
                UniformItem uniform = new PureArrayUniform(id, activeInfo, addr);
                addUniform(container, uniform);

            }
            else if (matches.Count == 3 || matches.Count == 4)
            {
                var id = matches[0].Value.Trim('.', '[', ']');
                var map = container.map.get(id);
                if (map == null)
                {
                    map = new StructuredUniform(id);
                    addUniform(container, map);
                }

                container = (UniformContainer)map;
                var idIndex = matches[1].Value.Trim('.', '[', ']');
                var idxMap = container.map.get(idIndex);
                if (idxMap == null)
                {
                    idxMap = new StructuredUniform(idIndex);
                    addUniform(container, idxMap);
                }

                container = (UniformContainer)idxMap;
                var subId = matches[2].Value.Trim('.', '[', ']');
                if (matches.Count == 3)
                {
                    var uniform = new SingleUniform(subId, activeInfo, addr);
                    addUniform(container, uniform);
                }
                else
                {
                    var uniform = new PureArrayUniform(subId, activeInfo, addr);
                    addUniform(container, uniform);
                }
            }
            else
            {
                throw new Exception("not supported.");
            }

        }

        public static void addUniform(UniformContainer container, IUniform uniformObject)
        {

            container.seq.Push(uniformObject);
            container.map[uniformObject.id] = uniformObject;

        }

        public void setValue(string name, object value, WebGLTextures textures = null)
        {
            if (this.map.TryGetValue(name, out IUniform u) && u != null)
            {
                u.setValue(value, textures);
            }
        }

        public void setOptional(object _object, string name, WebGLTextures textures = null)
        {
            if (_object.HasField(name))
            {
                var v = _object.GetField(name);

                this.setValue(name, v, textures);
            }
        }

        public static void upload(ListEx<IUniform> seq, Uniforms values, WebGLTextures textures)
        {

            for (int i = 0, n = seq.Length; i != n; ++i)
            {
                var u = seq[i];
                var v = values[u.id];

                if (v.needsUpdate == null || v.needsUpdate != false)
                {

                    // note: always updating when .needsUpdate is undefined
                    u.setValue(v.value, textures);
                }
            }
        }

        public static ListEx<IUniform> seqWithValue(ListEx<IUniform> seq, Uniforms values)
        {
            var r = new ListEx<IUniform>();

            for (int i = 0, n = seq.Length; i != n; ++i)
            {

                var u = seq[i];
                if (values.has(u.id)) r.Push(u);
            }
            return r;
        }
    }
}
